1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.services.ajax;
14  
15  import org.apache.tapestry5.Asset;
16  import org.apache.tapestry5.BooleanHook;
17  import org.apache.tapestry5.ComponentResources;
18  import org.apache.tapestry5.FieldFocusPriority;
19  import org.apache.tapestry5.func.F;
20  import org.apache.tapestry5.func.Worker;
21  import org.apache.tapestry5.internal.InternalConstants;
22  import org.apache.tapestry5.internal.services.DocumentLinker;
23  import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
24  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
25  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
26  import org.apache.tapestry5.ioc.util.IdAllocator;
27  import org.apache.tapestry5.json.JSONArray;
28  import org.apache.tapestry5.json.JSONObject;
29  import org.apache.tapestry5.services.javascript.*;
30  
31  import java.util.*;
32  
33  public class JavaScriptSupportImpl implements JavaScriptSupport
34  {
35      private final IdAllocator idAllocator;
36  
37      private final DocumentLinker linker;
38  
39      // Using a Map as a case-insensitive set of stack names.
40  
41      private final Map<String, Boolean> addedStacks = CollectionFactory.newCaseInsensitiveMap();
42  
43      private final Set<String> otherLibraries = CollectionFactory.newSet();
44  
45      private final Set<String> importedStylesheetURLs = CollectionFactory.newSet();
46  
47      private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList();
48  
49      private final List<InitializationImpl> inits = CollectionFactory.newList();
50  
51      private final JavaScriptStackSource javascriptStackSource;
52  
53      private final JavaScriptStackPathConstructor stackPathConstructor;
54  
55      private final boolean partialMode;
56  
57      private final BooleanHook suppressCoreStylesheetsHook;
58  
59      private FieldFocusPriority focusPriority;
60  
61      private String focusFieldId;
62  
63      private Map<String, String> libraryURLToStackName, moduleNameToStackName;
64  
65      class InitializationImpl implements Initialization
66      {
67          InitializationPriority priority = InitializationPriority.NORMAL;
68  
69          final String moduleName;
70  
71          String functionName;
72  
73          JSONArray arguments;
74  
75          InitializationImpl(String moduleName)
76          {
77              this.moduleName = moduleName;
78          }
79  
80          public Initialization invoke(String functionName)
81          {
82              assert InternalUtils.isNonBlank(functionName);
83  
84              this.functionName = functionName;
85  
86              return this;
87          }
88  
89          public Initialization priority(InitializationPriority priority)
90          {
91              assert priority != null;
92  
93              this.priority = priority;
94  
95              return this;
96          }
97  
98          public void with(Object... arguments)
99          {
100             assert arguments != null;
101 
102             this.arguments = new JSONArray(arguments);
103         }
104     }
105 
106     public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
107                                  JavaScriptStackPathConstructor stackPathConstructor, BooleanHook suppressCoreStylesheetsHook)
108     {
109         this(linker, javascriptStackSource, stackPathConstructor, new IdAllocator(), false, suppressCoreStylesheetsHook);
110     }
111 
112     /**
113      * @param linker
114      *         responsible for assembling all the information gathered by JavaScriptSupport and
115      *         attaching it to the Document (for a full page render) or to the JSON response (in a partial render)
116      * @param javascriptStackSource
117      *         source of information about {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s, used when handling the import
118      *         of libraries and stacks (often, to handle transitive dependencies)
119      * @param stackPathConstructor
120      *         encapsulates the knowledge of how to represent a stack (which may be converted
121      *         from a series of JavaScript libraries into a single virtual JavaScript library)
122      * @param idAllocator
123      *         used when allocating unique ids (it is usually pre-initialized in an Ajax request to ensure
124      *         that newly allocated ids do not conflict with previous renders and partial updates)
125      * @param partialMode
126      *         if true, then the JSS configures itself for a partial page render (part of an Ajax request)
127      *         which automatically assumes the "core" library has been added (to the original page render)
128      * @param suppressCoreStylesheetsHook
129      *         a hook that enables ignoring CSS files on the core stack
130      */
131     public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource,
132                                  JavaScriptStackPathConstructor stackPathConstructor, IdAllocator idAllocator, boolean partialMode,
133                                  BooleanHook suppressCoreStylesheetsHook)
134     {
135         this.linker = linker;
136         this.idAllocator = idAllocator;
137         this.javascriptStackSource = javascriptStackSource;
138         this.stackPathConstructor = stackPathConstructor;
139         this.partialMode = partialMode;
140         this.suppressCoreStylesheetsHook = suppressCoreStylesheetsHook;
141 
142         // In partial mode, assume that the infrastructure stack is already present
143         // (from the original page render).
144 
145         if (partialMode)
146         {
147             addedStacks.put(InternalConstants.CORE_STACK_NAME, true);
148         }
149     }
150 
151     public void commit()
152     {
153         if (focusFieldId != null)
154         {
155             require("t5/core/pageinit").invoke("focus").with(focusFieldId);
156         }
157 
158         F.flow(stylesheetLinks).each(new Worker<StylesheetLink>()
159         {
160             public void work(StylesheetLink value)
161             {
162                 linker.addStylesheetLink(value);
163             }
164         });
165 
166         F.flow(inits).sort(new Comparator<InitializationImpl>()
167         {
168             public int compare(InitializationImpl o1, InitializationImpl o2)
169             {
170                 return o1.priority.compareTo(o2.priority);
171             }
172         }).each(new Worker<InitializationImpl>()
173         {
174             public void work(InitializationImpl element)
175             {
176                 linker.addInitialization(element.priority, element.moduleName, element.functionName, element.arguments);
177             }
178         });
179     }
180 
181     public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter)
182     {
183         createInitializer(priority).with(functionName, parameter);
184     }
185 
186     public void addInitializerCall(String functionName, JSONArray parameter)
187     {
188         addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
189     }
190 
191     public void addInitializerCall(InitializationPriority priority, String functionName,
192                                    JSONArray parameter)
193     {
194         // TAP5-2300: In 5.3, a JSONArray implied an array of method arguments, so unwrap and add
195         // functionName to the arguments
196 
197         List parameterList = new ArrayList(parameter.length() + 1);
198         parameterList.add(functionName);
199         parameterList.addAll(parameter.toList());
200         createInitializer(priority).with(parameterList.toArray());
201     }
202 
203     private Initialization createInitializer(InitializationPriority priority)
204     {
205         assert priority != null;
206 
207         importCoreStack();
208 
209         return require("t5/core/init").priority(priority);
210     }
211 
212     public void addInitializerCall(String functionName, JSONObject parameter)
213     {
214         addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
215     }
216 
217     public void addInitializerCall(InitializationPriority priority, String functionName, String parameter)
218     {
219         createInitializer(priority).with(functionName, parameter);
220     }
221 
222     public void addInitializerCall(String functionName, String parameter)
223     {
224         addInitializerCall(InitializationPriority.NORMAL, functionName, parameter);
225     }
226 
227     public void addScript(InitializationPriority priority, String format, Object... arguments)
228     {
229         assert priority != null;
230         assert InternalUtils.isNonBlank(format);
231 
232         importCoreStack();
233 
234         String newScript = arguments.length == 0 ? format : String.format(format, arguments);
235 
236         if (partialMode)
237         {
238             require("t5/core/pageinit").invoke("evalJavaScript").with(newScript);
239         } else
240         {
241             linker.addScript(priority, newScript);
242         }
243     }
244 
245     public void addScript(String format, Object... arguments)
246     {
247         addScript(InitializationPriority.NORMAL, format, arguments);
248     }
249 
250     public void addModuleConfigurationCallback(ModuleConfigurationCallback callback)
251     {
252         linker.addModuleConfigurationCallback(callback);
253     }
254 
255     public String allocateClientId(ComponentResources resources)
256     {
257         return allocateClientId(resources.getId());
258     }
259 
260     public String allocateClientId(String id)
261     {
262         return idAllocator.allocateId(id);
263     }
264 
265     public JavaScriptSupport importJavaScriptLibrary(Asset asset)
266     {
267         assert asset != null;
268 
269         return importJavaScriptLibrary(asset.toClientURL());
270     }
271 
272     public JavaScriptSupport importJavaScriptLibrary(String libraryURL)
273     {
274         importCoreStack();
275 
276         String stackName = findStackForLibrary(libraryURL);
277 
278         if (stackName != null)
279         {
280             return importStack(stackName);
281         }
282 
283         if (!otherLibraries.contains(libraryURL))
284         {
285             linker.addLibrary(libraryURL);
286 
287             otherLibraries.add(libraryURL);
288         }
289 
290         return this;
291     }
292 
293     private void importCoreStack()
294     {
295         addAssetsFromStack(InternalConstants.CORE_STACK_NAME);
296     }
297 
298     /**
299      * Locates the name of the stack that includes the library URL. Returns the stack,
300      * or null if the library is free-standing.
301      */
302     private String findStackForLibrary(String libraryURL)
303     {
304         return getLibraryURLToStackName().get(libraryURL);
305     }
306 
307 
308     private Map<String, String> getLibraryURLToStackName()
309     {
310         if (libraryURLToStackName == null)
311         {
312             libraryURLToStackName = CollectionFactory.newMap();
313 
314             for (String stackName : javascriptStackSource.getStackNames())
315             {
316                 for (Asset library : javascriptStackSource.getStack(stackName).getJavaScriptLibraries())
317                 {
318                     libraryURLToStackName.put(library.toClientURL(), stackName);
319                 }
320             }
321         }
322 
323         return libraryURLToStackName;
324     }
325 
326     private String findStackForModule(String moduleName)
327     {
328         return getModuleNameToStackName().get(moduleName);
329     }
330 
331     private Map<String, String> getModuleNameToStackName()
332     {
333 
334         if (moduleNameToStackName == null)
335         {
336             moduleNameToStackName = CollectionFactory.newMap();
337 
338             for (String stackName : javascriptStackSource.getStackNames())
339             {
340                 for (String moduleName : javascriptStackSource.getStack(stackName).getModules())
341                 {
342                     moduleNameToStackName.put(moduleName, stackName);
343                 }
344             }
345         }
346 
347         return moduleNameToStackName;
348     }
349 
350 
351     private void addAssetsFromStack(String stackName)
352     {
353         if (addedStacks.containsKey(stackName))
354         {
355             return;
356         }
357 
358         JavaScriptStack stack = javascriptStackSource.getStack(stackName);
359 
360         for (String dependentStackname : stack.getStacks())
361         {
362             addAssetsFromStack(dependentStackname);
363         }
364 
365         addedStacks.put(stackName, true);
366 
367         boolean addAsCoreLibrary = stackName.equals(InternalConstants.CORE_STACK_NAME);
368 
369         List<String> libraryURLs = stackPathConstructor.constructPathsForJavaScriptStack(stackName);
370 
371         for (String libraryURL : libraryURLs)
372         {
373             if (addAsCoreLibrary)
374             {
375                 linker.addCoreLibrary(libraryURL);
376             } else
377             {
378                 linker.addLibrary(libraryURL);
379             }
380         }
381 
382         if (!(addAsCoreLibrary && suppressCoreStylesheetsHook.checkHook()))
383         {
384             stylesheetLinks.addAll(stack.getStylesheets());
385         }
386 
387         String initialization = stack.getInitialization();
388 
389         if (initialization != null)
390         {
391             addScript(InitializationPriority.IMMEDIATE, initialization);
392         }
393     }
394 
395     public JavaScriptSupport importStylesheet(Asset stylesheet)
396     {
397         assert stylesheet != null;
398 
399         return importStylesheet(new StylesheetLink(stylesheet));
400     }
401 
402     public JavaScriptSupport importStylesheet(StylesheetLink stylesheetLink)
403     {
404         assert stylesheetLink != null;
405 
406         importCoreStack();
407 
408         String stylesheetURL = stylesheetLink.getURL();
409 
410         if (!importedStylesheetURLs.contains(stylesheetURL))
411         {
412             importedStylesheetURLs.add(stylesheetURL);
413 
414             stylesheetLinks.add(stylesheetLink);
415         }
416 
417         return this;
418     }
419 
420     public JavaScriptSupport importStack(String stackName)
421     {
422         assert InternalUtils.isNonBlank(stackName);
423 
424         importCoreStack();
425 
426         addAssetsFromStack(stackName);
427 
428         return this;
429     }
430 
431     public JavaScriptSupport autofocus(FieldFocusPriority priority, String fieldId)
432     {
433         assert priority != null;
434         assert InternalUtils.isNonBlank(fieldId);
435 
436         if (focusFieldId == null || priority.compareTo(focusPriority) > 0)
437         {
438             this.focusPriority = priority;
439             focusFieldId = fieldId;
440         }
441 
442         return this;
443     }
444 
445     public Initialization require(String moduleName)
446     {
447         assert InternalUtils.isNonBlank(moduleName);
448 
449         importCoreStack();
450 
451         String stackName = findStackForModule(moduleName);
452 
453         if (stackName != null)
454         {
455             importStack(stackName);
456         }
457 
458         InitializationImpl init = new InitializationImpl(moduleName);
459 
460         inits.add(init);
461 
462         return init;
463     }
464 
465 }